"use strict";

/*
This program is released under the 
Creative Commons Attribution-ShareAlike 3.0 United States license
https://creativecommons.org/licenses/by-sa/3.0/us/
*/

var matrixData, fields, matrixTranspose, caseInfo, totalCases,
    independent, dependent, control,
    getSimilarity, getDifference;

var epsilon = 1e-5,
    rowBuilderId = null,
    rowToRender = 0,
    isRendering = false,
    peekTop = Infinity,
    peekLeft = Infinity,
    highlightCases = [],
    peek = {
	"top": 0,
	"left": 0,
	"right": Infinity,
	"bottom": Infinity
    },
    isUpToDate = {
	"data":  false,
	"table": false,
	"plot":  false
    };

$(setup);

function setup() {
    // jquery-ui
    $("#main").tabs({
	"disabled": [1,2],
	"heightStyle": "fill"
    }).addClass("ui-tabs-vertical ui-helper-clearfix");
    $("#variables ul").sortable({
	"connectWith": "#variables ul",
	"receive": prepare
    });
    $("#table").tooltip({
	"content": function() {
	    return $(this).attr("title").replace(/; /g,"<br>");
	}
    });
    $("#variables").tooltip();
    $("#svg").tooltip();

    // add the handlers
    $("#main > ul > li").click(analyze)
	.removeClass("ui-corner-top").addClass("ui-corner-left");
    $("#upload").change(parseFile);
    $("#selectTable").change(function() {
	isUpToDate.table = false;
    });
    $("#selectPlot").change(function() {
	isUpToDate.plot = false;
    });
    $(".horiz").scroll(synchScroll("horiz","scrollLeft"));
    $(".vert").scroll(synchScroll("vert","scrollTop"));
    $("#table table").on("click","td,th",tableClicked);
    $("#svg").on("click","circle",svgClicked);
    $("#main>ul>li>a[href=#table]").one("click",function() {
	$(".horiz").outerWidth($("#table").width()-$("#top").position().left);
	$(".vert").outerHeight($("#table").height()-$("#left").position().top);
    });

    // sizing
    $("#rotate").css('height',$('#rotate').width()*1.2)
	.css('width',$('table#headers tr:first-child').height());
    // make the tabs as tall as possible.  'fill' doesn't seem to do it
    var boxThick = $("#main").outerHeight(true)-$("#main").height(),
	availableHeight = $("html").height()-$("#main").offset().top-2*boxThick;
    $("#main").height(availableHeight);
    boxThick = $("#setup").outerHeight(true)-$("#setup").height();
    availableHeight -= boxThick;
    $("#main > div").height(availableHeight);
    // svg needs hardcoded dimensions
    availableHeight -= boxThick+2;
    $("#svgBox").height(availableHeight).width(availableHeight);
    $("#svg").height(availableHeight).width(availableHeight);
    availableHeight -= $("table#headers").offset().top;
    $("tr#variables td").css({"max-height":availableHeight,
			      "height":availableHeight})
	.outerHeight(availableHeight);
}

function tableClicked(event) {
    var node = $(event.target),
	parentDiv = node.parents("table").parent(),
	hasVert = parentDiv.hasClass("vert"),
	hasHoriz = parentDiv.hasClass("horiz"),
	colOffset = 0;
    highlightCases = [];
    if ( hasVert ) {
	var row = node.parents("tr").index();
	if ( row !== peek.top ) {
	    colOffset += peek.left;
	}
	highlightCases.push(row);
    }
    if ( hasHoriz ) {
	highlightCases.push(node.index()+colOffset);
    }
    updateHighlighting();
}

function svgClicked(event) {
    var index = $(event.target).index("circle");
    highlightCases = [index];
    updateHighlighting();
    $("a[href=#table]").one("click",function() {
	$("#right").scrollLeft($("#right table").width()*index/totalCases
			       -$("#right").width()/2)
	    .scrollTop($("#right table").height()*index/totalCases
		       -$("#right").height()/2);
    });
}

function updateHighlighting() {
    $(".highlight").removeClass("highlight");
    $("circle[fill=red]").attr("fill","black");
    $.each(highlightCases,function(ignoreIndex,caze) {
	$("#left tr").eq(caze).addClass("highlight");
	$("#top th").eq(caze).addClass("highlight");
	$("#right colgroup").eq(caze).addClass("highlight");
	$("#right tr").eq(caze).addClass("highlight");
	$("circle").eq(caze).attr("fill","red");
    });
}

function synchScroll(axis,scrollDir) {
    var initer = { "horiz":null, "vert":null };
    return function(event) {
	if ( initer[axis] !== null ) {
	    initer[axis] = null;
	    return;
	}
	initer[axis] = event.target;
	$("."+axis).not(initer[axis])[scrollDir]($(initer[axis])[scrollDir]());
	// in other words, $(".vert").scrollTop($(".vert").scrollTop());
	// using the initer's scroll to set the other scroll
	// (and avoiding an infinite loop)
    };
}

function parseFile() {
    $("#upload").parse({
	"config": {
	    "error": onError,
	    "complete": onLoader,
	    "dynamicTyping": true
	}
    });
}

function onError(err,file) {
    alert("Error loading: "+err.name);
    console.log(err);
    console.log(file);
}

function onLoader(results) {
    prepare();
    matrixData = results.data;
    fields = matrixData.shift();
    fields.shift();
    for ( var i = matrixData.length-1 ; i >= 0 ; i-- ) {
	if ( matrixData[i].length === 1 && matrixData[i][0] === "" ) {
	    matrixData.splice(i,1);
	}
    }
    caseInfo = $.map(matrixData,function(row) {
	return {"name":row.shift()};
    });
    totalCases = caseInfo.length;
    makeCategoricalNumeric();
    $("#variables ul").empty();
    $.each(fields,function(index,field) {
	$('<li title="'+field+'">'+field+"</li>")
	    .appendTo($("#ignored")).data("index",index);
    });
    center();
    $("#main").tabs("enable");
    $("#explanation button").click(downloadData);
    var outputCases = totalCases*(totalCases-1)/2,
	sizeEst = outputCases*140, // experimentally determined magic number
	suffixIndex = 0,
	suffixes = [" bytes"," Kb"," Mb"," Gb"," Tb"," Pb"," Eb"," Zb"," Yb"];
    // or maybe just "you're kidding, right?"
    while ( sizeEst > 1000 ) {
	suffixIndex++;
	sizeEst /= 1000;
    }
    // or suffixIndex = Math.floor(Math.log(sizeEst)/Math.log(1000))
    $("#sizeEst").remove();
    $("<p></p>",{"id":"sizeEst"})
	.text("About "+round(sizeEst,2)+suffixes[suffixIndex])
	.insertAfter("button");
}

function makeCategoricalNumeric() {
    for ( var field = 0 ; field < fields.length ; field++ ) {
	var nonNumeric = [];
	for ( var rowNum = 0 ; rowNum < totalCases ; rowNum++ ) {
	    if ( ! $.isNumeric(matrixData[rowNum][field]) ) {
		var value = nonNumeric.indexOf(matrixData[rowNum][field]);
		matrixData[rowNum][field] = value >= 0
		    ? value : nonNumeric.push(matrixData[rowNum][field]) - 1;
	    }
	}
    }
}

function center() {
    matrixTranspose = numeric.transpose(matrixData);
    $.each(matrixTranspose,function(ignoreIndex,column) {
	var mean = numeric.sum(column) / caseInfo.length;
	numeric.subeq(column,mean);
    });
    matrixData = numeric.transpose(matrixTranspose);
}

function prepare() {
    isUpToDate.data = false;
    isUpToDate.table = false;
    isUpToDate.plot = false;
    if ( rowBuilderId !== null ) {
	clearInterval(rowBuilderId);
	rowBuilderId = null;
    }
    var svg = $("#svg").svg("get");
    if ( svg ) {
	svg.clear();
    }
    $("#selectPlot option").each(function(ignoreIndex,option) {
	var text = $(option).text();
	if ( text === "No plot" ) {
	    return true; // continue to next element
	}
	var words = text.split(/\s+/);
	$(option).prop("disabled",
		       $("#"+words[1]+" li").length === 0
		       || $("#"+words[3]+" li").length === 0 );
    });
}

function analyze() {
    startTime = Date.now();
    if ( ! isUpToDate.data ) {
	updateMatrices();
    }
    if ( control.hasVariables
	 || independent.hasVariables
	 || dependent.hasVariables ) {
	plot();
	table();
    }
}

function Data(id) {
    var items = $("#"+id+" li");
    this.projector = numeric.rep([fields.length],0),
    this.fields = items.map(function(ignoreIndex,li) {
	return $(li).text();
    }).get();
    this.hasVariables = this.fields.length > 0;
    this.getDistance = function(a,b) {
	return 0;
    };
    if ( items.length !== 0 ) {
	var indicesInFields = items.map(function(ignoreIndex,li) {
	    return $(li).data("index");
	}).get().sort(),
	    dataTranspose = [];
	$.each(indicesInFields,function(ignoreIndex,indexInFields) {
	    dataTranspose.push(matrixTranspose[indexInFields]);
	}); // $.map flattens everything by 1, we could counter
	var data = numeric.transpose(dataTranspose),
	    covariance = numeric.dot(dataTranspose,data),
	    // /= totalCases-1 => *= totalCases-1 later
	    svd = numeric.svd(covariance),
	    mainEig = numeric.transpose(svd.V)[0];
	// numeric can't find the eigenstructure if it's singular,
	// but the svd gives the eigenvectors
	for ( var i = 0 ; i < indicesInFields.length ; i++ ) {
	    this.projector[indicesInFields[i]] = mainEig[i];
	} // can't use _this_.projector in $.each
	var diagInv = $.map(svd.S,function(elt) {
	    return ( Math.abs(elt) < epsilon ? 0 : 1/elt );
	}),
	    S_inv = numeric.diag(diagInv),
	    covInv = numeric.dot(numeric.dot(svd.V,S_inv),
				 numeric.transpose(svd.U));
	numeric.muleq(covInv,totalCases-1); // cov /= cases-1
	this.getDistance = function (a,b) {
	    var diff = numeric.sub(data[a],data[b]),
		distSq = numeric.dot(numeric.dot(diff,covInv),diff);
	    return Math.sqrt(distSq);
	}
    }
    this.reorder = function(newOrder) {
	data = reorder(data,newOrder);
    };
}

function updateMatrices() {
    // clear out the big objects for garbage collection
    control = undefined;
    independent = undefined;
    dependent = undefined;
    control = new Data("control");
    if ( $('#reorder').prop("checked") && control.hasVariables ) {
	sortMatrices();
    }
    independent = new Data("independent");
    dependent = new Data("dependent");
    isUpToDate.data = true;
    isUpToDate.table = false;
    isUpToDate.plot = false;
    setScorers();
}

function sortMatrices() {
    var ctrlDists = [];
    for ( var row1 = 0 ; row1 < totalCases ; row1++ ) {
	for ( var row2 = row1+1 ; row2 < totalCases ; row2++ ) {
	    ctrlDists.push([row1,row2]);
	}
    }
    ctrlDists.sort(function(a,b) {
	return control.getDistance(a[0],a[1])-control.getDistance(b[0],b[1]);
    });
    var newOrder = getNewOrder(ctrlDists);
    matrixData = reorder(matrixData,newOrder,false);
    matrixTranspose = numeric.transpose(matrixData);
    caseInfo = reorder(caseInfo,newOrder,false);
    control.reorder(newOrder);
}

/** Reorders the cases to have small control distances next to each other.
 *  This is done by greedily grabbing the smallest control distance that doesn't
 *  involve a variable that's already been grabbed twice. */
function getNewOrder(ctrlDists) {
    var degree = [],
	sortedSegments = [];
    for ( var i = 0 ; i < ctrlDists.length ; i++ ) {
	var current = ctrlDists[i];
	if ( ! degree[current[0]] ) {
	    degree[current[0]] = 0;
	}
	if ( ! degree[current[1]] ) {
	    degree[current[1]] = 0;
	}
	if ( degree[current[0]] >= 2 || degree[current[1]] >= 2 ) {
	    continue;
	}
	if ( degree[current[0]] === 0 && degree[current[1]] === 0 ) {
	    sortedSegments.push([current[0],current[1]]);
	} else if ( degree[current[0]] === 0 ) {
	    for ( var seg_i = 0 ; seg_i < sortedSegments.length ; seg_i++ ) {
		if ( sortedSegments[seg_i][0] === current[1] ) {
		    sortedSegments[seg_i].reverse();
		} // ensures the next "if" will be true
		if ( sortedSegments[seg_i][sortedSegments[seg_i].length-1]
		     === current[1] ) {
		    sortedSegments[seg_i].push(current[0]);
		    break;
		}
	    }
	} else if ( degree[current[1]] === 0 ) {
	    for ( var seg_i = 0 ; seg_i < sortedSegments.length ; seg_i++ ) {
		if ( sortedSegments[seg_i][0] === current[0] ) {
		    sortedSegments[seg_i].reverse();
		} // ensures the next "if" will be true
		if ( sortedSegments[seg_i][sortedSegments[seg_i].length-1]
		     === current[0] ) {
		    sortedSegments[seg_i].push(current[1]);
		    break;
		}
	    }
	} else { // join two sorted segments
	    var seg1, seg2;
	    for ( var seg_i = 0 ; seg_i < sortedSegments.length ; seg_i++ ) {
		if ( sortedSegments[seg_i][0] === current[0] ) {
		    sortedSegments[seg_i].reverse();
		} // ensures the next "if" will be true
		if ( sortedSegments[seg_i][sortedSegments[seg_i].length-1]
		     === current[0] ) {
		    seg1 = seg_i;
		}
		if ( sortedSegments[seg_i][sortedSegments[seg_i].length-1]
		     === current[1] ) {
		    sortedSegments[seg_i].reverse();
		} // ensures the next "if" will be true
		if ( sortedSegments[seg_i][0] === current[1] ) {
		    seg2 = seg_i;
		}
	    }
	    if ( seg1 === seg2 ) {
		continue;
	    } else {
		sortedSegments[seg1]
		    = sortedSegments[seg1].concat(sortedSegments[seg2]);
		sortedSegments.splice(seg2,1);
	    }
	}
	degree[current[0]]++;
	degree[current[1]]++;
    }
    if ( sortedSegments.length > 1 ) {
	throw "illegal";
    }
    return sortedSegments[0];
}

function reorder(toReorder,newOrder,transpose) {
    var reordered = [];
    if ( transpose ) {
	toReorder = numeric.transpose(toReorder);
    }
    for ( var i = 0 ; i < newOrder.length ; i++ ) {
	reordered.push(toReorder[newOrder[i]]);
    }
    if ( transpose ) {
	reordered = numeric.transpose(reordered);
    }
    return reordered;
}

function plot() {
    if ( isUpToDate.plot ) {
	return;
    }
    $("#svg").svg();
    $("#svg").svg("get").clear();
    var selectedText = $("#selectPlot :selected").text();
    if ( selectedText === "No plot" ) {
	return;
    }
    $("#main").tabs("enable","#plot");
    selectedText = selectedText.replace("Plot ","");
    var xKey = selectedText.replace(/ v .*$/,""),
	yKey = selectedText.replace(/^.* v /,"");
    project(window[xKey].projector,window[yKey].projector);
    draw();
    isUpToDate.plot = true;
}

function project(xPi,yPi) {
    $.each(caseInfo,function(index,caze) {
	caze.xLoc = numeric.dot(matrixData[index],xPi);
	caze.yLoc = numeric.dot(matrixData[index],yPi);
    });
}

function draw() {
    var xVals = $.map(caseInfo,function(caze) {return caze.xLoc;}),
	yVals = $.map(caseInfo,function(caze) {return caze.yLoc;}),
	xMax = max(xVals),
	xMin = min(xVals),
	yMax = max(yVals),
	yMin = min(yVals),
	xSpread = xMax-xMin,
	ySpread = yMax-yMin,
	svg = $("#svg").svg("get"),
	width = $("#svg").width(),
	height = $("#svg").height(),
	xScale = width/xSpread,
	yScale = height/ySpread;
    svg.clear();
    svg.line(0,height/2,width,height/2,{"strokeWidth":1,"stroke":"black"});
    svg.line(width/2,0, width/2,height,{"strokeWidth":1,"stroke":"black"});
    $.each(caseInfo,function(ignoreIndex,caze) {
	if ( caze.xLoc && caze.yLoc && caze.name ) {
	    svg.circle(xScale*(caze.xLoc-xMin),
		       yScale*(caze.yLoc-yMin),
		       2, // the radius
		       {"fill":"black","title":caze.name});
	}
    });
}

function table() {
    if ( isUpToDate.table ) {
	return;
    }
    $("#top thead").empty().html("<tr></tr>");
    $("#left thead").empty();
    $("#right table").empty().html("<tbody></tbody>");
    rowToRender = 0;
    if ( $("#selectTable :selected").text() === "No table design" ) {
	return;
    }
    var topNames = [],
	leftNames = [];
    var caseName;
    for ( var rowIndex = 0 ; rowIndex < totalCases ; rowIndex++ ) {
	caseName = caseInfo[rowIndex].name;
	topNames.push('</th><th title="',caseName,'">',caseName);
	leftNames.push('</th></tr><tr><th title="',caseName,'">',caseName);
    }
    topNames[0] = '<th title="';
    topNames.push("</th>");
    leftNames[0] = '<tr><th title="';
    leftNames.push("</th></tr>");
    $("#top tr").append(topNames.join(''));
    $("#left thead").append(leftNames.join(''));
    $("#right table").prepend("<colgroup></colgroup>".repeat(totalCases));
    // before the tbody

    $("#main").tabs("enable","#table");
    isUpToDate.table = true;
    if ( $("#bigdata").prop("checked") ) {
	blankLongLine = '<tr><td colspan="'+totalCases+'">\xA0</td></tr>';
	blankFilledLine = "<td>\xA0</td>".repeat(totalCases);
	$("#right").scroll(checkPeek);
	tablePeek();
    } else {
	rowBuilderId = setInterval(renderRow,1);
    }
}

function checkPeek() {
    if ( Math.abs( peekTop-$("#right").scrollTop() ) > $("#right").height()*.9
	 || Math.abs( peekLeft-$("#right").scrollLeft() ) > $("#right").width()*.9 ) {
	tablePeek();
    }
}

var blankLongLine, blankFilledLine;

// supposedly, colspan > 1000 resets to colspan=1
// but that doesn't seem to happen
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/td

function tablePeek() {
    peekTop = $("#right").scrollTop(),
    peekLeft = $("#right").scrollLeft();
    var rightHeight = $("#right").height(),
	tableHeight = $("#left table").height(),
	rightWidth = $("#right").width(),
	tableWidth = $("#top table").width(),
	table = [],
	getDistance
	= $("#selectTable :selected").text() === "Most Similar design"
	? getSimilarity : getDifference;
    peek.top = Math.floor((peekTop-rightHeight)*totalCases/tableHeight);
    peek.left = Math.floor((peekLeft-rightWidth)*totalCases/tableWidth),
    peek.bottom = Math.ceil((peekTop+2*rightHeight)*totalCases/tableHeight);
    peek.right = Math.ceil((peekLeft+2*rightWidth)*totalCases/tableWidth);

    peek.top = Math.max(peek.top,0);
    peek.bottom = Math.min(peek.bottom,totalCases);
    peek.left = Math.max(peek.left,0);
    peek.right = Math.min(peek.right,totalCases);
    // start filling the table
    table.push(blankLongLine.repeat(peek.top));
    table.push("<tr>");
    table.push(('<td rowspan="'+(peek.bottom-peek.top)+'">\xA0</td>')
	       .repeat(peek.left));
    for ( var col = peek.left ; col < peek.right ; col++ ) {
	table.push(getTd(peek.top,col,getDistance));
    }
    table.push(('<td rowspan="'+(peek.bottom-peek.top)+'">\xA0</td>')
	       .repeat(totalCases-peek.right));
    table.push("</tr>");
    for ( var row = peek.top+1 ; row < peek.bottom ; row++ ) {
	table.push("<tr>");
	for ( var col = peek.left ; col < peek.right ; col++ ) {
	    table.push(getTd(row,col,getDistance));
	}
	table.push("</tr>");
    }
    table.push(blankLongLine.repeat(totalCases-peek.bottom));
    $("#right tbody").html(table.join(''));
    updateHighlighting();
}

var startTime;

function renderRow() {
    if ( isRendering ) {
	return; // keeps two render calls from overlapping
    }
    isRendering = true;
    var	newTrHtml = ["<tr>"],
	getDistance
	= $("#selectTable :selected").text() === "Most Similar design"
	? getSimilarity : getDifference;
    if ( rowToRender >= totalCases ) {
	clearInterval(rowBuilderId);
	rowBuilderId = null;
	isRendering = false;
//	console.log("ms rendering: "+(Date.now()-startTime));
	return;
    }
    for ( var colIndex = 0 ; colIndex < totalCases ; colIndex++ ) {
	newTrHtml.push(getTd(rowToRender,colIndex,getDistance));
    }
    newTrHtml.push("</tr>");
    rowToRender++;
    $("#right tbody").append(newTrHtml.join(''));
    isRendering = false;
}

function getTd(rowIndex,colIndex,getDistance) {
    if ( rowIndex === colIndex ) {
	return '<td></td>';
    } else {
	var c = control.getDistance(rowIndex,colIndex),
	    d = dependent.getDistance(rowIndex,colIndex),
	    i = independent.getDistance(rowIndex,colIndex);
	return '<td title="Control Variable distance: '+round(c,2)
	    +"; Dependent Variable distance: "+round(d,2)
	    +"; Independent Variable distance: "+round(i,2)
	    +'">'+round(getDistance(c,d,i),2)+"</td>";
    }
}

function setScorers() {
    var switchee = ( dependent.hasVariables ? 1 : 0 )
	+ ( independent.hasVariables ? 2 : 0 )
	+ ( control.hasVariables ? 4 : 0 );
    switch ( switchee ) {
    case 0:
	getSimilarity = function(c,d,i) { return 1; }
	getDifference = function(c,d,i) { return 1; }
	break;
    case 1:
	getSimilarity = function(c,d,i) { return Math.sqrt(d); }
	getDifference = function(c,d,i) { return 1/d; }
	break;
    case 2:
	getSimilarity = function(c,d,i) { return Math.sqrt(i); }
	getDifference = function(c,d,i) { return 1/i; }
	break;
    case 3:
	getSimilarity = function(c,d,i) { return Math.sqrt(i*d); }
	getDifference = function(c,d,i) { return 1/(i+d); }
	break;
    case 4:
	getSimilarity = function(c,d,i) { return 1/c; }
	getDifference = function(c,d,i) { return c; }
	break;
    case 5:
	getSimilarity = function(c,d,i) { return Math.sqrt(d)/c; }
	getDifference = function(c,d,i) { return c/d; }
	break;
    case 6:
	getSimilarity = function(c,d,i) { return Math.sqrt(i)/c; }
	getDifference = function(c,d,i) { return c/i; }
	break;
    case 7:
	getSimilarity = function(c,d,i) { return Math.sqrt(i*d)/c; }
	getDifference = function(c,d,i) { return c/(i+d); }
	break;
    default: throw "no such case";
    }
}

function downloadData() {
    if ( ! isUpToDate.data ) {
	updateMatrices();
    }
    var csvData = [
	["Comparative Case Identification Analysis"],
	["Control variables"].concat(control.fields),
	["Independent variables"].concat(independent.fields),
	["Dependent variables"].concat(dependent.fields),
	[],
	[
	    "Case 1","Case 2","Independent distance","Dependent distance",
	    "Control distance","Similarity","Difference"
	]
    ];
    $("#bigPcheck").nextAll().remove();
    if ( $("#bigdata").prop("checked") ) {
	$("<p></p>").appendTo("#explanation")
	    .text("Downloading results is not advised for a large number of cases");
	return;
    }
    for ( var rowIindex = 0 ; rowIindex < totalCases ; rowIindex++ ) {
	for ( var rowJindex = 0 ; rowJindex < totalCases ; rowJindex++ ) {
	    if ( rowIindex < rowJindex ) {
		var i = independent.getDistance(rowIindex,rowJindex),
		    d = dependent.getDistance(rowIindex,rowJindex),
		    c = control.getDistance(rowIindex,rowJindex);
		csvData.push([
		    caseInfo[rowIindex].name,
		    caseInfo[rowJindex].name,
		    i,
		    d,
		    c,
		    getSimilarity(c,d,i),
		    getDifference(c,d,i)
		]);
	    }
	}
    }
    var type = "text/csv;charset=utf-8",
	downloadSucceeded = false;
    if ( !!window.saveAs ) {
	try {
	    var blob = new Blob([Papa.unparse(csvData)],{"type":type});
	    saveAs(blob,"download.csv");
	    downloadSucceeded = true;
	    console.log("primary downloading");
	} catch (e) {
	    downloadSucceeded = false;
	}
    }
    if ( ! downloadSucceeded ) {
	var downloader = $('<a download href="data:'+type+','
			   +escape(Papa.unparse(csvData))+'"></a>')
	    .appendTo("body")
	downloader[0].click();
	downloader.remove();
	console.log("alternate downloading");
	downloadSucceeded = true;
    }
}

function round(numin,digits) {
    var oneEdigits = Math.pow(10,digits);
    return Math.round(numin*oneEdigits)/oneEdigits;
}

function max(array) {
    return Math.max.apply(null,array);
}

function min(array) {
    return Math.min.apply(null,array);
}

// the following is from
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/repeat#Polyfill
if (!String.prototype.repeat) {
    String.prototype.repeat = function(count) {
	'use strict';
	if (this == null) {
	    throw new TypeError('can\'t convert ' + this + ' to object');
	}
	var str = '' + this;
	count = +count;
	if (count != count) {
	    count = 0;
	}
	if (count < 0) {
	    throw new RangeError('repeat count must be non-negative');
	}
	if (count == Infinity) {
	    throw new RangeError('repeat count must be less than infinity');
	}
	count = Math.floor(count);
	if (str.length == 0 || count == 0) {
	    return '';
	}
	// Ensuring count is a 31-bit integer allows us to heavily optimize the
	// main part. But anyway, most current (august 2014) browsers can't
	// handle strings 1 << 28 chars or longer, so:
	if (str.length * count >= 1 << 28) {
	    throw new RangeError('resulting string too long');
	}
	var rpt = '';
	for (;;) {
	    if ((count & 1) == 1) {
		rpt += str;
	    }
	    count >>>= 1;
	    if (count == 0) {
		break;
	    }
	    str += str;
	}
	return rpt;
    }
}
